﻿using Server.EventHandlers;
using Server.Packets;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    /// <summary>
    /// A Connection server receiving IPV4 connect request and answering with IPV6 server address.
    /// </summary>
    public sealed class ConnectionServer : IDisposable
    {
        #region Events

        /// <summary>
        /// Occurs when an error is raised by the <see cref="T:Server.ConnectionServer" />.
        /// </summary>
        private event GenericEventHandler<ConnectionServer, ExceptionEventArgs> m_onConnectionServerError;

        /// <summary>
        /// Occurs when a client is connected to the <see cref="T:Server.ConnectionServer" />.
        /// </summary>
        private event GenericEventHandler<ConnectionServer, TcpClientEventArgs> m_onConnectionServerClientConnected;

        #endregion

        #region Fields

        /// <summary>
        /// A lock <see cref="T:System.Object" />.
        /// </summary>
        private readonly object m_lock;

        /// <summary>
        /// The maximum length of the pending connections queue.
        /// </summary>
        private readonly int m_pendingConnectionsQueueLength;

        /// <summary>
        /// The <see cref="T:Server.ConnectionInformations" />.
        /// </summary>
        private readonly ConnectionInformations m_connectionInformations;

        /// <summary>
        /// The port on which to listen for incoming connection attempts.
        /// </summary>
        /// <remarks>Port must be between <see cref="F:System.Net.IPEndPoint.MinPort" /> and <see cref="F:System.Net.IPEndPoint.MaxPort" />.</remarks>
        private readonly int m_port;

        /// <summary>
        /// The number of milliseconds during which the <see cref="T:Server.ConnectionServer" /> will be sleeping when it has no work to do.
        /// </summary>
        private readonly int m_serverIdleTime;

        /// <summary>
        /// The <see cref="T:System.Text.Encoding" /> format.
        /// </summary>
        private readonly Encoding m_encoding;

        /// <summary>
        /// The <see cref="T:Server.TcpServer" /> related to this <see cref="T:Server.ConnectionServer" />.
        /// </summary>
        private readonly TcpServer m_tcpServer;

        /// <summary>
        /// Indicates whether the <see cref="T:Server.ConnectionServer" /> is started.
        /// </summary>
        private bool m_isStarted;

        /// <summary>
        /// Indicates whether the connections should be handled.
        /// </summary>
        private bool m_handleConnections;

        /// <summary>
        /// The <see cref="T:System.Net.Sockets.TcpListener" />.
        /// </summary>
        private TcpListener m_clientListener;

        /// <summary>
        /// The connections handler <see cref="T:System.Threading.Tasks.Task" />.
        /// </summary>
        private Task m_connectionsHandlerTask;

        #endregion

        #region Constructors

        /// <summary>
        /// Used to initialize static data.
        /// </summary>
        /// <remarks>It is called automatically before the first instance is created or any static members are referenced.</remarks>
        static ConnectionServer()
        {
            Packet.Define(new S0ConnectionPacket());
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:Server.ConnectionServer" /> class.
        /// </summary>
        /// <param name="tcpServer">The related <see cref="T:Server.TcpServer" />.</param>
        /// <param name="connectionInformations">The <see cref="T:Server.ConnectionInformations" />.</param>
        /// <param name="port">The port on which to listen for incoming connection attempts.</param>
        /// <param name="pendingConnectionsQueueLength">The maximum length of the pending connections queue.</param>
        /// <param name="serverIdleTime">The number of milliseconds during which the server will be sleeping when it has no work to do.</param>
        /// <param name="encoding">The character encoding method.</param>
        /// <param name="onConnectionServerError">Represents the method that will handle the error event of this object.</param>
        /// <param name="onConnectionServerClientConnected">Represents the method that will handle the client connection event of this object.</param>
        /// <exception cref="T:System.Collections.Generic.KeyNotFoundException">The <see cref="T:Server.AddressPacket" /> type was not mapped.</exception>
        internal ConnectionServer(TcpServer tcpServer, ConnectionInformations connectionInformations, int port, int pendingConnectionsQueueLength, int serverIdleTime, Encoding encoding, GenericEventHandler<ConnectionServer, ExceptionEventArgs> onConnectionServerError,
            GenericEventHandler<ConnectionServer, TcpClientEventArgs> onConnectionServerClientConnected)
        {
            Contract.Requires<ArgumentNullException>(tcpServer != null);
            Contract.Requires<ArgumentOutOfRangeException>(port >= IPEndPoint.MinPort && port <= IPEndPoint.MaxPort);
            Contract.Requires<ArgumentOutOfRangeException>(pendingConnectionsQueueLength >= 0);
            Contract.Requires<ArgumentOutOfRangeException>(serverIdleTime >= 0);
            Contract.Requires<ArgumentNullException>(encoding != null);

            m_lock = new object();
            m_connectionInformations = connectionInformations;
            m_port = port;
            m_pendingConnectionsQueueLength = pendingConnectionsQueueLength;
            m_serverIdleTime = serverIdleTime;
            m_encoding = encoding;
            m_tcpServer = tcpServer;
            m_onConnectionServerError = onConnectionServerError;
            m_onConnectionServerClientConnected = onConnectionServerClientConnected;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Define the invariant valid state for this class.
        /// </summary>
        [ContractInvariantMethod]
        private void InvariantValidState()
        {
            Contract.Invariant(m_lock != null);
            Contract.Invariant(m_pendingConnectionsQueueLength >= 0);
            Contract.Invariant(m_port >= IPEndPoint.MinPort && m_port <= IPEndPoint.MaxPort);
            Contract.Invariant(m_serverIdleTime >= 0);
            Contract.Invariant(m_encoding != null);
            Contract.Invariant(m_tcpServer != null);
        }

        /// <summary>
        /// Start the <see cref="T:Server.ConnectionServer" />.
        /// </summary>
        /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred while accessing the socket.</exception>
        /// <exception cref="T:System.InvalidOperationException">The underlying <see cref="T:System.Net.Sockets.Socket" /> is null.</exception>
        internal void Start()
        {
            Contract.Assert(m_lock != null);
            Contract.Assert(m_pendingConnectionsQueueLength >= 0);

            // prevent starting if server is already started
            if (m_isStarted) return;

            lock (m_lock)
            {
                // prevent concurrent start
                if (m_isStarted) return;

                // server is started
                m_isStarted = true;

                // start listening for incoming connection requests
                try
                {
                    m_clientListener = new TcpListener(IPAddress.Any, m_port);
                    m_clientListener.Start(m_pendingConnectionsQueueLength);
                }
                catch (SystemException e)
                {
                    HandleException(e);
                }

                // start the connection handler
                m_handleConnections = true;
                m_connectionsHandlerTask = Task.Factory.StartNew(ConnectionsHandler);
            }
        }

        /// <summary>
        /// Stop the <see cref="T:Server.ConnectionServer" />.
        /// </summary>
        /// <exception cref="T:System.ObjectDisposedException">The connection handler task has been disposed.</exception>
        /// <exception cref="T:System.AggregateException">The connection handler task was canceled -or- an exception was thrown during its execution.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">Use the <see cref="P:System.Net.Sockets.SocketException.ErrorCode" /> property to obtain the specific error code. When you have obtained this code, you can refer to the Windows Sockets version 2 API error code documentation in MSDN for a detailed description of the error.</exception>
        internal void Stop()
        {
            Contract.Assert(m_lock != null);
            Contract.Assert(m_serverIdleTime >= 0);

            // prevent stopping if server is not started
            if (!m_isStarted) return;

            lock (m_lock)
            {
                // prevent concurrent stop
                if (!m_isStarted) return;

                // server is stopped
                m_isStarted = false;

                // stop the handler tasks
                m_handleConnections = false;
                try
                {
                    if (m_connectionsHandlerTask != null && m_connectionsHandlerTask.Wait(m_serverIdleTime))
                        m_connectionsHandlerTask.Dispose();
                }
                catch (ObjectDisposedException e)
                {
                    HandleException(e);
                }
                catch (AggregateException e)
                {
                    HandleException(e);
                }
                finally
                {
                    m_connectionsHandlerTask = null;

                    // stop the client listener
                    try
                    {
                        if (m_clientListener != null)
                        {
                            m_clientListener.Stop();
                            m_clientListener = null;
                        }
                    }
                    catch (SocketException e)
                    {
                        HandleException(e);
                    }
                    finally
                    {
                        GC.Collect();
                    }
                }
            }
        }

        /// <summary>
        /// Handle the incoming connections.
        /// </summary>
        /// <exception cref="T:System.InvalidOperationException">The listener has not been started with a call to <see cref="M:System.Net.Sockets.TcpListener.Start" /> -or- The <see cref="T:System.Net.Sockets.TcpClient" /> is not connected to a remote host.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">Use the <see cref="P:System.Net.Sockets.SocketException.ErrorCode" /> property to obtain the specific error code. When you have obtained this code, you can refer to the Windows Sockets version 2 API error code documentation in MSDN for a detailed description of the error.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.TcpClient" /> has been closed -or- The stream is closed.</exception>
        /// <exception cref="T:System.ArgumentException">The stream does not support writing or is already closed.</exception>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred when attempting to access the socket -or - The address family is <see cref="F:System.Net.Sockets.AddressFamily.InterNetworkV6" /> and the address is bad.</exception>
        /// <exception cref="T:System.InvalidOperationException">The remote IP could not be determined.</exception>
        private void ConnectionsHandler()
        {
            Contract.Assert(m_serverIdleTime >= 0);

            // loop while connections should be handled
            while (m_handleConnections)
            {
                try
                {
                    // there is a pending connection request
                    if (m_clientListener != null && m_clientListener.Pending())
                    {
                        // handle the new client
                        using (TcpClient client = m_clientListener.AcceptTcpClient())
                        {
                            HandleClient(client);
                        }
                        Thread.Yield();
                    }
                    // wait until the next pending connection request check
                    else
                        Thread.Sleep(m_serverIdleTime);
                }
                catch (SystemException e)
                {
                    HandleException(e);
                }
            }
        }

        /// <summary>
        /// Handle a <see cref="T:System.Net.Sockets.TcpClient" />.
        /// </summary>
        /// <param name="client">The <see cref="T:System.Net.Sockets.TcpClient" />.</param>
        /// <exception cref="T:System.InvalidOperationException">The <see cref="T:System.Net.Sockets.TcpClient" /> is not connected to a remote host.</exception>
        /// <exception cref="T:System.ObjectDisposedException">The <see cref="T:System.Net.Sockets.TcpClient" /> has been closed.</exception>
        /// <exception cref="T:System.ArgumentException">The stream does not support writing or is already closed.</exception>
        /// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">An error occurred when attempting to access the socket -or - The address family is <see cref="F:System.Net.Sockets.AddressFamily.InterNetworkV6" /> and the address is bad.</exception>
        /// <exception cref="T:System.InvalidOperationException">The remote IP could not be determined.</exception>
        private void HandleClient(TcpClient client)
        {
            Contract.Requires<ArgumentNullException>(client != null);
            Contract.Assert(m_encoding != null);

            client.NoDelay = true;

            // raise the m_onConnectionServerClientConnected event if defined
            if (m_onConnectionServerClientConnected != null)
                Task.Factory.StartNew(() => m_onConnectionServerClientConnected(this, new TcpClientEventArgs(client)));

            try
            {
                string clientRemoteIp = client.GetRemoteIp();

                // add the client address to the tcp server pending clients addresses
                m_tcpServer.PendingClientsAddresses.Add(clientRemoteIp);

                // send the server address to the client
                using (var writer = new BinaryWriter(client.GetStream(), m_encoding, true))
                {
                    // send to the client its ipv4 address, and the server ipv6 address and port
                    writer.Write(new S0ConnectionPacket(m_connectionInformations, clientRemoteIp).WriteData(m_encoding));
                }
            }
            catch (SystemException e)
            {
                HandleException(e);
            }
        }

        /// <summary>
        /// Handle an <see cref="T:System.Exception" />.
        /// </summary>
        /// <param name="e">The <see cref="T:System.Exception" /> that occured.</param>
        private void HandleException(Exception e)
        {
            // raise the m_onConnectionServerError event if defined
            if (m_onConnectionServerError == null)
                throw e;
            Task.Factory.StartNew(() => m_onConnectionServerError(this, new ExceptionEventArgs(e)));
            Thread.Yield();
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Implementation of <see cref="T:System.IDisposable" />.
        /// </summary>
        /// <exception cref="T:System.ObjectDisposedException">The connection handler task has been disposed.</exception>
        /// <exception cref="T:System.AggregateException">The connection handler task was canceled -or- an exception was thrown during its execution.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">Use the <see cref="P:System.Net.Sockets.SocketException.ErrorCode" /> property to obtain the specific error code. When you have obtained this code, you can refer to the Windows Sockets version 2 API error code documentation in MSDN for a detailed description of the error.</exception>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Finalizer.
        /// </summary>
        /// <remarks>Simply calls <code>Dispose(false)</code>.</remarks>
        ~ConnectionServer()
        {
            Dispose(false);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> if managed resources should be disposed; otherwise <c>false</c>.</param>
        /// <exception cref="T:System.ObjectDisposedException">The connection handler task has been disposed.</exception>
        /// <exception cref="T:System.AggregateException">The connection handler task was canceled -or- an exception was thrown during its execution.</exception>
        /// <exception cref="T:System.Net.Sockets.SocketException">Use the <see cref="P:System.Net.Sockets.SocketException.ErrorCode" /> property to obtain the specific error code. When you have obtained this code, you can refer to the Windows Sockets version 2 API error code documentation in MSDN for a detailed description of the error.</exception>
        private void Dispose(bool disposing)
        {
            // the finalizer didn't calls for this
            if (disposing)
                Stop();
        }

        #endregion
    }
}
